<?php
namespace doujunyu\utility\tencen\wechat;
//require_once('vendor/autoload.php');
use think\Exception;
use WeChatPay\Builder;
use WeChatPay\Crypto\AesGcm;
use WeChatPay\Crypto\Rsa;
use WeChatPay\Util\PemUtil;
use WeChatPay\Formatter;

// 设置参数

class WeChatPayV3{

    // |-------------------------
    // | 代码辅助备忘
    // | "https://".$_SERVER['HTTP_HOST']  获取当前网站域名地址
    // |-------------------------
    //在linux上执行查看证书的序列号
    // openssl x509 -in apiclient_cert.pem -noout -serial
    //获取序列号之后，生成微信公钥
    // php vendor/bin/CertificateDownloader.php -k {V3密钥} -m {商户ID} -f {apiclient_key.pem} -s {序列号} -o {文件生成后的位置}

    // 商户号
    protected $merchantId = '1695703398';
    // 使用证书生成器生成的apiclient_key.pem文件
    protected $merchantPrivateKeyFilePath = 'file:///www/wwwroot/item-gas-station/public/cert/oil_kangkang/apiclient_key.pem';
    // 账户中心-》api安全-》商户api证书-》管理证书-》证书序列号
    protected $merchantCertificateSerial = '1E90950AD4976F8952291722DA7E53BDCF489601';
    // 平台证书使用下面静态方法self_cmd_cert生成命令去执行后会得到一个文件
    // 微信支付公钥：账户中心-》api安全-》微信支付公钥-》xxxxxx
    protected $platformCertificateFilePath = 'file:///www/wwwroot/item-gas-station/public/cert/pub_key.pem';//生成v3给的商品key
    // 平台证书序列号位置：账户中心-》api安全-》平台证书-》管理证书-》当前证书序列号
    // 微信支付公钥序列号位置：账户中心-》api安全-》微信支付公钥-》xxxxxx
    protected $platformCertificateSerialOrPublicKeyId = 'PUB_KEY_ID_0116957033982024101900505400000011';
    // protected $platformCertificateFilePath = 'file:///www/wwwroot/item-gas-station/public/cert/pub_key.pem';//商品平台上下载下来的公钥证书
    protected $apiv3Key = 'MLGqXyROyAY6Q6mg52uHqLLGZdj5uJ5V';

    protected $instance = null;

    /**
     * $merchantId = 商户号
     * $merchantPrivateKeyFilePath = 使用证书生成器生成的apiclient_key.pem文件
     * $merchantCertificateSerial = 账户中心-》api安全-》商户api证书-》管理证书-》证书序列号
     * $platformCertificateFilePath = 微信支付公钥    或者    平台证书
     * $platformCertificateSerialOrPublicKeyId = 微信公钥或平台证书对应的序列号
     * $apiv3Key = 密钥
     */
    public function __construct(string $merchantId, string $merchantPrivateKeyFilePath, string $merchantCertificateSerial, string $platformCertificateFilePath, string $platformCertificateSerialOrPublicKeyId, string $apiv3Key)
    {
        if (!class_exists(Builder::class)) {
            throw  new  Exception("请先执行 'composer require wechatpay/wechatpay'");
        }
        //商户ID
        $this->merchantId = $merchantId;
        //apiclient_key.pem
        $this->merchantPrivateKeyFilePath = $merchantPrivateKeyFilePath;
        //apiclient_serial通过命令生openssl x509 -in apiclient_cert.pem -noout -serial获取到的
        $this->merchantCertificateSerial = $merchantCertificateSerial;
        //通过php vendor/bin/CertificateDownloader.php生成的证书
        $this->platformCertificateFilePath = $platformCertificateFilePath;
        //商户平台 -> 账户中心 -> API安全 -> 平台证书 获取到的
        $this->platformCertificateSerialOrPublicKeyId = $platformCertificateSerialOrPublicKeyId;
        //V3密钥
        $this->apiv3Key = $apiv3Key;

        // 商户号
        $merchantId = $this->merchantId;
        // 从本地文件中加载「商户API私钥」，「商户API私钥」会用来生成请求的签名
        $merchantPrivateKeyFilePath = $this->merchantPrivateKeyFilePath;
        $merchantPrivateKeyInstance = Rsa::from($merchantPrivateKeyFilePath, Rsa::KEY_TYPE_PRIVATE);
        // 「商户API证书」的「证书序列号」
        $merchantCertificateSerial = $this->merchantCertificateSerial;
        // 从本地文件中加载「微信支付平台证书」或者「微信支付平台公钥」，用来验证微信支付应答的签名
        $platformCertificateOrPublicKeyFilePath = $this->platformCertificateFilePath;
        $platformPublicKeyInstance = Rsa::from($platformCertificateOrPublicKeyFilePath, Rsa::KEY_TYPE_PUBLIC);

        // 「微信支付平台证书」的「证书序列号」或者是「微信支付平台公钥ID」
        // 「平台证书序列号」及/或「平台公钥ID」可以从 商户平台 -> 账户中心 -> API安全 直接查询到
        $platformCertificateSerialOrPublicKeyId = $this->platformCertificateSerialOrPublicKeyId;
        // 构造一个 APIv3 客户端实例
        $this->instance = Builder::factory([
            'mchid'      => $merchantId,
            'serial'     => $merchantCertificateSerial,
            'privateKey' => $merchantPrivateKeyInstance,
            'certs'      => [
                $platformCertificateSerialOrPublicKeyId => $platformPublicKeyInstance,
            ],
        ]);


    }

    /** 使用证书模式的时候可以使用这个进行拼接，把参数填写上，生成可执行的命令
     *  $merchantId = 商户号
     *  $apiv3Key = v3密钥
     *  $merchantPrivateKeyFilePath = 商户API证书key位置
     *  $merchantCertificateSerial = 商户API证书cert序列号
     */
    public static function self_cmd_cert($merchantId,$apiv3Key,$merchantPrivateKeyFilePath,$merchantCertificateSerial,$path){
        return "php vendor/bin/CertificateDownloader.php -k {$apiv3Key} -m {$merchantId} -f {$merchantPrivateKeyFilePath} -s {$merchantCertificateSerial} -o {$path}";
    }



    /** 小程序下单/jsapi下单
     * @param string $appid 小程序app_id
     * @param string $out_trade_no 订单
     * @param int $amount 金额（分）
     * @param string $openid 小程序用户open_id
     * @param string $description 商品描述
     * @param string $notify_url 回调地址
     * @return array|string[]
     */
    public function mini(string $appid, string $out_trade_no,int $amount,string $openid, string $description, string $notify_url,bool $settle_info = false){
        try {
            // 以 Native 支付为例，发送请求 wxd136c3204c142892
            $resp = $this->instance
                ->chain('v3/pay/transactions/jsapi')
                ->post(['json' => [
                    'mchid'        => $this->merchantId,
                //    'out_trade_no' => 'native1217752501201407033'.random_int(1000,9999),
                    'out_trade_no' => $out_trade_no,
                //    'appid'        => 'wx84609af61c3f787d',
                    'appid'        => $appid,
                    'description'  => $description,
                    'notify_url'   => $notify_url,
                    'amount'       => [
                        'total'    => $amount,
                        'currency' => 'CNY'
                    ],
                    'payer'=>[
                    //    'openid'=> 'o2BqX7ThBpFZUpmo3LbawW78GJR8'
                        'openid'=> $openid
                    ],
                    'settle_info' => [
                        'profit_sharing' => $settle_info
                    ]
                ]
                ]);

        } catch (\Exception $e) {
            // 进行错误处理
            if ($e instanceof \GuzzleHttp\Exception\RequestException && $e->hasResponse()) {
                $resp = $e->getResponse();
            }else{
                return ['小程序支付执行错误',''];
            }
        }
        $body_arr = json_decode($resp->getBody(),true);
        if(!isset($body_arr['prepay_id']) || empty($body_arr['prepay_id'])){
            return [$body_arr['message'],''];
        }
        return ['',$body_arr['prepay_id']];
    }

    /** native下单
     *  https://pay.weixin.qq.com/doc/v3/merchant/4012791877
     */
    public function pay_native(string $appid, string $description, string $out_trade_no, string $notify_url, int $amount, bool $settle_info = false){
        try {
            // 以 Native 支付为例，发送请求 wxd136c3204c142892
            $resp = $this->instance
                ->chain('v3/pay/transactions/native')
                ->post(['json' => [
                    'mchid'        => $this->merchantId,
                    'appid'        => $appid,
                    'description'  => $description,
                //    'out_trade_no' => 'native1217752501201407033'.random_int(1000,9999),
                    'out_trade_no' => $out_trade_no,
                    
                   
                    'notify_url'   => $notify_url,
                    'amount'       => [
                        'total'    => $amount,
                        'currency' => 'CNY'
                    ],
                    'settle_info' => [
                        'profit_sharing' => $settle_info
                    ]
                ]
                ]);

        } catch (\Exception $e) {
            // 进行错误处理
            if ($e instanceof \GuzzleHttp\Exception\RequestException && $e->hasResponse()) {
                $resp = $e->getResponse();
            }else{
                return ['小程序支付执行错误',''];
            }
        }
        $body_arr = json_decode($resp->getBody(),true);
        if(!isset($body_arr['code_url']) || empty($body_arr['code_url'])){
            return [$body_arr['message'],''];
        }
        return ['',$body_arr['code_url']];
    }


    /**
     * 退款
     * @param array $body
     * [
     *      'transaction_id' => 选填 string(32) 【微信支付订单号】原支付交易对应的微信订单号，与out_trade_no二选一
     *      'out_trade_no' => 选填 string(32) 【商户订单号】原支付交易对应的商户订单号，与transaction_id二选一
     *      'out_refund_no' => 必填 string(64) 【商户退款单号】商户系统内部的退款单号，商户系统内部唯一，只能是数字、大小写字母_-|*@ ，同一退款单号多次请求只退一笔。
     *      'reason' => 选填 string(80) 【退款原因】若商户传入，会在下发给用户的退款消息中体现退款原因
     *      'notify_url' => 选填 string(256) 【退款结果回调url】异步接收微信支付退款结果通知的回调地址，通知url必须为外网可访问的url，不能携带参数。 如果参数中传了notify_url，则商户平台上配置的回调地址将不会生效，优先回调当前传的这个地址
     *      'funds_account' => 选填 string 【退款资金来源】若传递此参数则使用对应的资金账户退款，否则默认使用未结算资金退款（仅对老资金流商户适用）可选取值：(AVAILABLE: 仅对老资金流商户适用，指定从可用余额账户出资,UNSETTLED: 仅对出行预付押金退款适用，指定从未结算资金出资)
     *      'amount' => [ //必填 object 【金额信息】订单金额信息
     *          'refund' => 必填 integer 【退款金额】退款金额，单位为分，只能为整数，不能超过原订单支付金额。
     *          'from' => 选填 array[object]
     *          'total' => 必填 integer 【原订单金额】原支付交易的订单总金额，单位为分，只能为整数
     *          'currency' => 必填 string(16)【退款币种】符合ISO 4217标准的三位字母代码，目前只支持人民币：CNY。
     *          'goods_detail' => 【退款商品】指定商品退款需要传此参数，其他场景无需传递
     *      ]
     * ]
     * @return array|string[]
     */
    public function refunds(array $body){
        try {
            // 以 Native 支付为例，发送请求 wxd136c3204c142892
            $resp = $this->instance
                ->chain('/v3/refund/domestic/refunds')
                ->post(['json' => $body]);
        } catch (\Exception $e) {
            // 进行错误处理
            if ($e instanceof \GuzzleHttp\Exception\RequestException && $e->hasResponse()) {
                $resp = $e->getResponse();
            }else{
                return ['退款方法执行错误',''];
            }
        }
        $body_arr = json_decode($resp->getBody(),true);
        if(isset($body_arr['code'])){
            return [$body_arr['message'],$body_arr];
        }
        if(!in_array($body_arr['status'],['SUCCESS','PROCESSING'])){
            return ['退款操作失败',$body_arr];
        }
        return ['',$body_arr];
    }

    // |-------------------------
    // | 分账
    // |-------------------------
    /**
     * 添加分账接收方
     * 文档地址:https://pay.weixin.qq.com/doc/v3/merchant/4012528995
     * @param array $body
     * [
     *      'appid' => 必填 string(32)
     *      'type' => 必填 string(32) 【接收方类型】枚举值：MERCHANT_ID：商户ID, PERSONAL_OPENID：个人openid（由父商户APPID转换得到）
     *      'account' => 必填 string(64) 【接收方账号】类型是MERCHANT_ID时，是商户号, 类型是PERSONAL_OPENID时，是个人openid
     *      'name' => 必填 string(32) 【分账接收方全称】分账接收方类型是MERCHANT_ID时，是商户全称（必传），当商户是小微商户或个体户时，是开户人姓名, 分账接收方类型是PERSONAL_OPENID时，是个人姓名（选传，传则校验）
     *      'relation_type' => 必填 string(32) 【与分账方的关系类型】子商户与接收方的关系,本字段值为枚举：SERVICE_PROVIDER：服务商, STORE：门店, STAFF：员工, STORE_OWNER：店主, PARTNER：合作伙伴, HEADQUARTER：总部, BRAND：品牌方, DISTRIBUTOR：分销商, DISTRIBUTOR：分销商, DISTRIBUTOR：分销商, USER：用户, SUPPLIER：供应商, CUSTOM：自定义
     *      'custom_relation' => 选填 string(10) 【自定义的分账关系】子商户与接收方具体的关系，本字段最多10个字。
     * ]
     * @return array|string[]
     */
    public function receivers_add($body){
        try {
            // 以 Native 支付为例，发送请求 wxd136c3204c142892
            $resp = $this->instance
                ->chain('/v3/profitsharing/receivers/add')
                ->post(['json' => $body]);
        } catch (\Exception $e) {
            // 进行错误处理
            if ($e instanceof \GuzzleHttp\Exception\RequestException && $e->hasResponse()) {
                $resp = $e->getResponse();
            }else{
                return ['添加分账错误',''];
            }
        }
        $body_arr = json_decode($resp->getBody(),true);
        if(isset($body_arr['code'])){
            return [$body_arr['message'],$body_arr];
        }
        return ['',$body_arr];
    }

    /**
     * 删除分账接收方
     * @param array $body
     * [
     *      'appid' => 必填 string(32)
     *      'type' => 必填 string(32) 【接收方类型】枚举值：MERCHANT_ID：商户ID, PERSONAL_OPENID：个人openid（由父商户APPID转换得到）
     *      'account' => 必填 string(64) 【接收方账号】类型是MERCHANT_ID时，是商户号, 类型是PERSONAL_OPENID时，是个人openid
     * ]
     * @return array|string[]
     */
    public function receivers_delete($body){
        try {
            // 以 Native 支付为例，发送请求 wxd136c3204c142892
            $resp = $this->instance
                ->chain('/v3/profitsharing/receivers/delete')
                ->post(['json' => $body]);
        } catch (\Exception $e) {
            // 进行错误处理
            if ($e instanceof \GuzzleHttp\Exception\RequestException && $e->hasResponse()) {
                $resp = $e->getResponse();
            }else{
                return ['删除分账错误',''];
            }
        }
        $body_arr = json_decode($resp->getBody(),true);
        if(isset($body_arr['code'])){
            return [$body_arr['message'],$body_arr];
        }
        return ['',$body_arr];
    }

    /**
     * 请求分账
     */
    public function profitsharing_orders($body){
        try {
            $resp = $this->instance
                ->chain('/v3/profitsharing/orders')
                ->post(['json' => $body]);
        } catch (\Exception $e) {
            // 进行错误处理
            if ($e instanceof \GuzzleHttp\Exception\RequestException && $e->hasResponse()) {
                $resp = $e->getResponse();
            }else{
                return ['请求分账错误',''];
            }
        }
        $body_arr = json_decode($resp->getBody(),true);
        if(isset($body_arr['code'])){
            return [$body_arr['message'],$body_arr];
        }
        return ['',$body_arr];
    }


    /**
     * 小程序签名后返回给前端，前端根据这个调起支付
     * @param string $prepay_id 创建订单返回的值
     * @return array
     */
    public function sign_to_view(string $prepay_id,string $app_id){

        $merchantPrivateKeyFilePath = $this->merchantPrivateKeyFilePath;
        $merchantPrivateKeyInstance = Rsa::from($merchantPrivateKeyFilePath);

        $params = [
            'appId'     => $app_id,
            'timeStamp' => (string)time(),
            'nonceStr'  => Formatter::nonce(),
            'package'   => 'prepay_id='.$prepay_id,
        ];
        $params += ['paySign' => Rsa::sign(
            Formatter::joinedByLineFeed(...array_values($params)),
            $merchantPrivateKeyInstance
        ), 'signType' => 'RSA'];
        return ['',$params];
    }

    /**回调验签
     *  返回验签失败案例：
     *      $response = Response::create(['code' => 'FAIL','message' => $err],'json',500);
     *      $response->code(500);
     *      return $response;
     */
    public function check_notify($headers){
        $inWechatpaySignature = $headers['wechatpay-signature'];// 请根据实际情况获取
        $inWechatpayTimestamp = $headers['wechatpay-timestamp'];// 请根据实际情况获取
        $inWechatpaySerial = $headers['wechatpay-serial'];// 请根据实际情况获取
        $inWechatpayNonce = $headers['wechatpay-nonce'];// 请根据实际情况获取
        $inBody = file_get_contents('php://input');// 请根据实际情况获取，例如: file_get_contents('php://input');

        $apiv3Key = $this->apiv3Key;// 在商户平台上设置的APIv3密钥

        // 根据通知的平台证书序列号，查询本地平台证书文件，
        // 假定为 `/path/to/wechatpay/inWechatpaySerial.pem`
        $platformPublicKeyInstance = Rsa::from($this->platformCertificateFilePath, Rsa::KEY_TYPE_PUBLIC);

        // 检查通知时间偏移量，允许5分钟之内的偏移
        $timeOffsetStatus = 300 >= abs(Formatter::timestamp() - (int)$inWechatpayTimestamp);
        $verifiedStatus = Rsa::verify(
        // 构造验签名串
            Formatter::joinedByLineFeed($inWechatpayTimestamp, $inWechatpayNonce, $inBody),
            $inWechatpaySignature,
            $platformPublicKeyInstance
        );
        if ($timeOffsetStatus && $verifiedStatus) {
            // 转换通知的JSON文本消息为PHP Array数组
            $inBodyArray = (array)json_decode($inBody, true);
            // 使用PHP7的数据解构语法，从Array中解构并赋值变量
            ['resource' => [
                'ciphertext'      => $ciphertext,
                'nonce'           => $nonce,
                'associated_data' => $aad
            ]] = $inBodyArray;
            // 加密文本消息解密
            $inBodyResource = AesGcm::decrypt($ciphertext, $apiv3Key, $nonce, $aad);
            // 把解密后的文本转换为PHP Array数组
            $inBodyResourceArray = (array)json_decode($inBodyResource, true);
            return ['',$inBodyResourceArray];
        }
        if(!$timeOffsetStatus){
            return ['通知时间超过5分钟',[]];
        }
        return ['失败',[]];
    }

}
